//
//  rendered.swift
//  Documentation
//
//  Created by Toj on 6/26/23.
//
//  推荐文章:
https://blog.csdn.net/a1661408343/article/details/106214655/

CPU (Central Processing Unit): 现代计算机整个系统的运算/ 控制核心单元.
GPU (Graphics Processing Unit): 可进行绘图运算工作的专用微处理器, 是连接计算机和显示终端的纽带.

GPU 普通渲染:
app -> frame buffer -> display
离屏渲染: 图层合并就会出现
app -> offscreenrender -> merge -> frame buffer -> display

// MARK: - 1. 图像的显示
/** 显示
 * 从 左 --> 右, 上 --> 下, 不一定是一帧为一个周期. 下面会解释为什么一帧不一定为一个周期
 *
 * 集成显卡
 * 帧缓冲区(内存) -> GPU视频控制器显示到屏幕
 *
 * 独立显卡
 * 帧缓冲区(内存) -> 显示处理器(显存) -> GPU视频控制器显示到屏幕
 */

/**
 * 撕裂
 * 单缓冲区
 * 1. 第1帧没有渲染完, 来一个新帧会从新帧同样位置读取, 这就是为什么不一定1帧为1个周期, 可能1个周期经历多帧.
 * 2. 缓冲区内没有下一帧数据, GPU会渲染上一帧的数据, 当渲染一半的时候 下一帧数据过来, 在同样位置渲染的下一帧数据.
 *
 * 垂直同步 + 多缓冲策略
 * 垂直同步: 一帧未渲染完, 不会渲染下一帧,
 * 在渲染第一针加锁, 在渲染完第一帧后解锁, 渲染下一帧
 * 下一帧进来放进临时缓冲区内, 垂直同步锁解锁后会读取临时缓冲区内下一帧
 * 掉帧: 当前帧已经结束, 帧缓冲区还没有下一帧数据, 还会渲染当前帧
 * 可以认为 1帧为1个周期
 */

// MARK: - 2. 渲染填充算法
/* 1. 画家算法: 也叫作优先填充
 ** 它是三维计算机图形学中处理可见性问题的一种解决方法.
 ** 当将三维场景投影到二维平面的时候, 需要确定哪些多边形是可见的, 哪些是不可见的
 ** 画家算法: 表示头脑简单的画家首先绘制距离较远的场景, 然后用绘制距离较近的场景覆盖较远的部分.
 ** 画家算法: 首先将场景中的多边形根据深度进行排序, 然后按照顺序进行描绘.
 ** 这种方法通常会将不可见的部分覆盖，这样就可以解决可见性问题
 ** 画家算法: 无法处理相互重叠的多边形在有些场合下, 画家算法可能无法解决可见性问题.
 ** 在这个例子中, 多边形 A、B、C 互相重叠, 我们无法确定哪一个多边形在上面, 哪一个在下面,
 我们也无法确定两个多边形什么时候在三维空间中交叉.
 ** 在这种情况下必须用一些方法对这些多边形进行切分、排序.
 ** 1972年提出的Newell算法就是切分类似多边形的一种方法, 在计算几何领域人们已经提出了许许多多的解决方法
 
 * 2. 逆向画家算法
 ** 这种算法首先绘制距离观察者较近的物体, 已经进行绘制的部分不再进行其它的绘制过程.
 ** 在计算机图形系统中, 这种方法由于无需根据光照、纹理等参数计算被较近物体遮挡的远处物体的颜色, 所以效率非常高.
 ** 但是，这种方法也有许多与普通画家算法同样的问题.
 
 * 3. 深度缓冲技术
 ** 画家算法的这些缺陷导致了深度缓冲技术的发展.
 ** 深度缓冲技术可以看作是画家算法的一个发展, 它根据逐个像素的信息解决深度冲突的问题
 并且抛弃了对于深度渲染顺序的依赖.
 ** 即使在这样的系统中, 有时也使用画家算法的变体.
 ** 由于深度缓冲实现通常是基于硬件中的固定精度深度缓冲寄存器,
 因此舍入误差就会带来一些显示问题, 即在多边形连接的地方会出现重叠或者间隙.
 ** 为了避免这种问题,一些图形处理引擎使用了“过度渲染”的方法, 即根据画家算法的顺序绘制两个多边形中受影响的边界.
 ** 这也就是说有些像素如同在画家算法中那样实际上绘制了两次,
 但是由于图像中只有很少的一部分才做这样的处理, 因此对于性能的影响很小.
 *
 * 4. OpenGL ES 渲染:
 ** OpenGL ES需要借助GL程序,
 ** 通过创建GL程序、顶点与片段着色器、加载着色器代码、编译代码、应用、数据填充,最终进行渲染
 *
 * 5. iOS 采用Metal渲染: 2014年推出一种低层次的渲染应用程序编程接口
 ** Metal渲染 借鉴了OpenGLES的渲染流程,
 ** 它通过控制顶点着色器/ 片元着色器(Metal里面叫顶点函数/片元函数), 交给帧缓冲区，最后显示到屏幕上.
 */

// MARK: - 3. iOS渲染流程
/*
 1. Core Animation: CPU
 Application: 点击事件/ 布局.... --> Rander Server: 解码处理draw cells
 
 1.2 Application:
 a. Handle Events: 这个过程中会先处理点击事件.
    这个过程中有可能会需要改变页面的布局和界面层次
 b. Commit Transaction: 处理显示内容的前置计算.
    比如布局计算、图片解码等任务, 接下来会进行详细的讲解.
    之后将计算好的图层进行打包发给 Render Serve
1.2. Rander Server:
 c. Decode: 会进行解码.
    注意完成解码之后需要等待下一个 RunLoop 才会执行下一步 Draw Calls.
 d. Draw Calls: 解码完成后, Core Animation 会调用下层渲染框架(OpenGL 或者 Metal)的方法进行绘制, 进而调用到 GPU.
 
 2. GPU --> render -> frame buffer
 
 3. 视频控制器(video controller) 读取frame buffer 经过数模转换 传递给 显示器 Display 显示
 */

 /*
 iOS图片解码:
 CPU 确定显示后才会通过Core Graphics 重绘解码, SDWebImage/ YYImage都是一样的逻辑, 只不过
 再通过GPU(OpenGL ES)渲染到屏幕上
 
// MARK: CPU 做准备阶段
 0. Application 应用处理阶段：得到图元
  
// MARK: GPU 渲染阶段
图谱:【Files --> 图像渲染流程.png】
  
 1. 几何处理单元geometry processing:
    a. 顶点着色器(Vertex Shader): 这个阶段中会将图元中的顶点信息进行视角转换、添加光照信息、增加纹理等操作.
    b. 形状装配(Shape Assembly): 图元中的三角形、线段、点分别对应三个 Vertex、两个 Vertex、一个 Vertex.
 这个阶段会将 Vertex 连接成相对应的形状.
    c. 几何着色器(Geometry Shader): 添加额外的Vertex, 将原始图元转换成新图元, 以构建一个不一样的模型.
 简单来说就是基于通过三角形、线段和点构建更复杂的几何图形.
 主要作用总结:
 ** 视图和投影矩阵对点的位置进行变化, 确定好图形位置(坐标系转换3维转2维),分割形状(三角形), 大小, 形状数据
 ** 如果需要利用法线的时候, 也同样需要利用视图矩阵对其进行转换
 ** 纹理坐标的产生和转换
 ** 顶点的光照或者象素光照的计算
 ** 颜色计算
 
 2. 光栅化Rasterization: 确定图形那些像素被填充, 从而将像素划分成不同的部分
 3. 像素点处理pixel processing: 片元着色器, 填充颜色
    a. 片段着色器(Fragment Shader): 也叫做 Pixel Shader, 这个阶段的目的是给每一个像素 Pixel 赋予正确的颜色.
 颜色的来源就是之前得到的顶点、纹理、光照等信息.
 由于需要处理纹理、光照等复杂信息, 所以这通常是整个系统的性能瓶颈.
 
    b. 测试与混合(Tests and Blending): 也叫做 Merging 阶段, 这个阶段主要处理片段的前后位置以及透明度.
 这个阶段会检测各个着色片段的深度值 z 坐标, 从而判断片段的前后位置, 以及是否应该被舍弃.
 同时也会计算相应的透明度 alpha 值, 从而进行片段的混合，得到最终的颜色.
 */

// MARK: - 4. Commit Transaction做了什么?
主要: Layout、Display、Prepare、Commit 等四个具体的操作
/**
 * 1. Layout: 视图构建
 调用重载的 layoutSubviews 方法
 创建视图, 并通过 addSubview 方法添加子视图
 计算视图布局, 即所有的 Layout Constraint
 由于 Commit Transaction 这个阶段是在 CPU 中进行, 通常是 CPU 限制或者 IO 限制,
 所以我们应该尽量高效轻量地操作, 减少这部分的时间,
 比如减少非必要的视图创建、简化布局计算、减少视图层级等.
 * 2. Display: 绘制视图
 这个阶段主要是交给 Core Graphics 进行视图的绘制,
 NOTE:
 * 注意不是真正的显示, 而是得到前文所说的图元 primitives 数据:
 2.1. 根据上一阶段 Layout 的结果创建得到图元信息.
 2.2. 如果重写了 drawRect: 方法, 那么会调用重载的 drawRect: 方法, 在 drawRect: 方法中手动绘制得到 bitmap 数据, 从而自定义视图的绘制.
 NOTE:
 * 注意正常情况下 Display 阶段只会得到图元 primitives 信息, 而位图 bitmap 是在 GPU 中根据图元信息绘制得到的.
 * 但是如果重写了 drawRect: 方法, 这个方法会直接调用 Core Graphics 绘制方法得到 bitmap 数据, 同时系统会额外申请一块内存, 用于暂存绘制好的 bitmap.

 由于重写了 drawRect: 方法, 导致绘制过程从 GPU 转移到了 CPU, 这就导致了一定的效率损失.
 与此同时, 这个过程会额外使用 CPU 和内存, 因此需要高效绘制, 否则容易造成 CPU 卡顿或者内存爆炸
 * 3. Prepare: Core Animation 额外的工作 图片解码和转换
 * 4. Commit: 打包并发送
 NOTE:
 * 注意 commit 操作是依赖图层树递归执行的, 所以如果图层树过于复杂, commit 的开销就会很大.
 * 这也是我们希望减少视图层级，从而降低图层树复杂度的原因
 */

// MARK: - 5. Render Server 做了什么?
图元信息 到达Render Server 后会被反序列化, 得到图层树,
按照图层树中图层顺序、RGBA值、图层frame过滤图层中被遮挡的部分,
过滤后将图层树转成渲染树, 渲染树的信息会转给GPU Render渲染

Render 通常是 OpenGL ES 或者是 Metal.
以 OpenGL 为例, 那么上图主要是 GPU 中执行的操作, 具体主要包括:

GPU 收到 Command Buffer, 包含图元 primitives 信息
1. Tiler 开始工作: 先通过顶点着色器 Vertex Shader 对顶点进行处理, 更新图元信息
2. 平铺过程: 平铺生成 tile bucket 的几何图形, 这一步会将图元信息转化为像素, 之后将结果写入 Parameter Buffer 中
Tiler 更新完所有的图元信息, 或者 Parameter Buffer 已满, 则会开始下一步
3. Renderer 工作: 将像素信息进行处理得到 bitmap，之后存入 Render Buffer
4. Render Buffer 中存储有渲染好的 bitmap，供之后的 Display 操作使用

// MARK: - 6. 离屏渲染 请看【off-screen-rendered.swift】

// MARK: - 7. CALayer 与 bitmap关系. 请看【CALayer、UIView.swift】

// MARK: - 8. 优化方案总结
1. 通过【Commit Transaction --> Layout or Commit】:
减少视图创建、简化布局计算、减少视图层级

2. 通过【Commit Transaction --> Display】:
drawRect方法能不重写尽量别重写, 会额外对CPU 内存增压, 造成卡顿.

使用 Instrument 的 OpenGL ES, 可以对过程进行监控.
OpenGL ES tiler utilization 和 OpenGL ES renderer utilization 可以分别监控 Tiler 和 Renderer 的工作情况
3. 离屏渲染, 尽量别触发离屏渲染, 离屏渲染避免方法【off-screen-rendered.swift】, 具体问题具体分析
离屏渲染未必就一定造成资源浪费/ 性能不好.
a. 一些特殊效果需要使用额外的 Offscreen Buffer 来保存渲染的中间状态, 所以不得不使用离屏渲染.
b. 处于效率目的, 可以将内容提前渲染保存在 Offscreen Buffer 中, 达到复用的目的
